Fix/version week - #533
Conversation
|
🔍 OpenCodeReview found 13 issue(s) in this PR.
📄
|
| child: Row( | ||
| mainAxisAlignment: MainAxisAlignment.center, | ||
| children: [ | ||
| // Filled, not outlined: the one active affordance on a page | ||
| // whose every other row is an outlined icon. | ||
| Container( | ||
| width: 44, | ||
| height: 44, | ||
| width: 34, | ||
| height: 34, | ||
| decoration: BoxDecoration( | ||
| shape: BoxShape.circle, | ||
| color: gold.badge, | ||
| ), | ||
| child: Icon(Icons.favorite, color: gold.onBadge, size: 24), | ||
| child: Icon(Icons.favorite, color: gold.onBadge, size: 19), | ||
| ), | ||
| Text( | ||
| l10n.sponsorTitle, | ||
| textAlign: TextAlign.center, | ||
| style: theme.textTheme.titleMedium?.copyWith( | ||
| fontWeight: FontWeight.w700, | ||
| color: gold.ink, | ||
| const SizedBox(width: AppSpacing.sm), | ||
| Flexible( | ||
| child: Text( | ||
| l10n.sponsorTitle, | ||
| maxLines: 1, | ||
| overflow: TextOverflow.ellipsis, | ||
| style: theme.textTheme.titleMedium?.copyWith( | ||
| fontWeight: FontWeight.w700, | ||
| color: gold.ink, | ||
| ), | ||
| ), | ||
| ), | ||
| const SizedBox(width: AppSpacing.xs), | ||
| Icon( | ||
| Icons.chevron_right, | ||
| size: 14, | ||
| color: gold.ink.withValues(alpha: 0.7), | ||
| ), | ||
| ], | ||
| ), |
There was a problem hiding this comment.
根據程式碼註解的設計意圖,「右側欄位應呈現為一個對齊的堆疊(the right column reads as one aligned stack)」,意指後方的箭頭(trailing arrow)應該在垂直方向上對齊。
然而,目前的實作使用了 MainAxisAlignment.center 搭配 Flexible,這會導致:
- 當文字較短時,整個
Row的內容會集中在中間,箭頭的位置會隨文字長度而左右移動,無法形成右側對齊的「列」。 Flexible不會強制佔用剩餘空間,只有在文字過長需要截斷時才會縮減。
建議改為:
- 將
MainAxisAlignment.center改為MainAxisAlignment.start(或移除,因為預設即為 start)。 - 將
Flexible改為Expanded。這樣文字會填滿中間的剩餘空間,將箭頭強制推至 Row 的最右側,從而達成註解所述的「右側對齊堆疊」效果。
Suggestion:
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| // Filled, not outlined: the one active affordance on a page | |
| // whose every other row is an outlined icon. | |
| Container( | |
| width: 44, | |
| height: 44, | |
| width: 34, | |
| height: 34, | |
| decoration: BoxDecoration( | |
| shape: BoxShape.circle, | |
| color: gold.badge, | |
| ), | |
| child: Icon(Icons.favorite, color: gold.onBadge, size: 24), | |
| child: Icon(Icons.favorite, color: gold.onBadge, size: 19), | |
| ), | |
| Text( | |
| l10n.sponsorTitle, | |
| textAlign: TextAlign.center, | |
| style: theme.textTheme.titleMedium?.copyWith( | |
| fontWeight: FontWeight.w700, | |
| color: gold.ink, | |
| const SizedBox(width: AppSpacing.sm), | |
| Flexible( | |
| child: Text( | |
| l10n.sponsorTitle, | |
| maxLines: 1, | |
| overflow: TextOverflow.ellipsis, | |
| style: theme.textTheme.titleMedium?.copyWith( | |
| fontWeight: FontWeight.w700, | |
| color: gold.ink, | |
| ), | |
| ), | |
| ), | |
| const SizedBox(width: AppSpacing.xs), | |
| Icon( | |
| Icons.chevron_right, | |
| size: 14, | |
| color: gold.ink.withValues(alpha: 0.7), | |
| ), | |
| ], | |
| ), | |
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.start, | |
| children: [ | |
| // Filled, not outlined: the one active affordance on a page | |
| // whose every other row is an outlined icon. | |
| Container( | |
| width: 34, | |
| height: 34, | |
| decoration: BoxDecoration( | |
| shape: BoxShape.circle, | |
| color: gold.badge, | |
| ), | |
| child: Icon(Icons.favorite, color: gold.onBadge, size: 19), | |
| ), | |
| const SizedBox(width: AppSpacing.sm), | |
| Expanded( | |
| child: Text( | |
| l10n.sponsorTitle, | |
| maxLines: 1, | |
| overflow: TextOverflow.ellipsis, | |
| style: theme.textTheme.titleMedium?.copyWith( | |
| fontWeight: FontWeight.w700, | |
| color: gold.ink, | |
| ), | |
| ), | |
| ), | |
| const SizedBox(width: AppSpacing.xs), | |
| Icon( | |
| Icons.chevron_right, | |
| size: 14, | |
| color: gold.ink.withValues(alpha: 0.7), | |
| ), | |
| ], | |
| ), |
Fix(zh-Hant): 修正週一上午建置的版本號會標成上一週 Fix(en-US): fix a Monday-morning build being named for the previous week The week came from UTC, and the people reading the label are eight hours ahead — so every Monday between 00:00 and 08:00 Taipei fell in the previous ISO week. `26w33e` was built at 07:47 that Monday and the build an hour later became `26w34a`: two consecutive snapshots, a week apart by name. Report dates already use Asia/Taipei (api.md); the label now does too.
Optimization(zh-Hant): 贊助卡片改為橫式排版,與右欄其他卡片對齊更好看 Optimization(en-US): the sponsor card now row-aligns with the right column
The More page will lead the version card with a stable anchor number: a release names itself and a snapshot names the release it builds toward. That value — the newest v* tag, v stripped — now rides every build the same way the label does, through version.sh, the generated build info, and CI's dart-define.
New(zh-Hant): 更多頁上方卡片重新設計,版本以漸層數字顯示,pre-release 顯示上一版主版本號 New(en-US): the More hero cards go flat, and the version card leads with a gradient major.minor named after the last release
Fix(zh-Hant): 修正圖層時間軸在某些時區會顯示成 UTC 時間 Fix(en-US): map timelines now show frame times in the device's local time instead of UTC
The version card's big number is the train (26.1) — the release a snapshot is cut toward — not the previous release tag. version.sh already derives it; this replaces the last-release pipeline with a train pipeline end to end: script output, generated build_info, the AppBuild surface, and the dart-defines CI stamps. No user-visible change yet; the card itself lands in the next commit.
The support, Discord and announcement cards each centred their row, so every icon and label started where its own row's text happened to end. Now they all start at the same left margin and the trailing arrows sit at the same right margin. Fix(zh-Hant): 修正「更多」頁上方三張卡片的圖示與文字起點對齊 Fix(en-US): align the icons and text of the three hero cards on the More page
The version card now reads 26.1 first — the train every build rides — and a snapshot names itself beside it in smaller print (26w34a). Both are larger than before so the number is the thing the eye lands on. New(zh-Hant): 「更多」頁版本卡改以版號 26.1 為主,測試版同時標示完整版號 New(en-US): the version card on the More page leads with 26.1 and shows the full snapshot name beneath it
New(zh-Hant): 更多頁英雄區加入伺服器狀態卡,並縮小版本卡 New(en-US): Add the server-status card to the More hero block and slim the version card
fee2df0 to
68ddee6
Compare
| static const double _versionHeight = 176; | ||
|
|
||
| /// Height of a small card (Discord, announcement, status) and, matching it, | ||
| /// the full-width support card below. | ||
| static const double _smallCardHeight = 56; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return SizedBox( | ||
| height: _height, | ||
| child: Padding( | ||
| padding: const EdgeInsets.fromLTRB( | ||
| AppSpacing.lg, | ||
| 0, | ||
| AppSpacing.lg, | ||
| AppSpacing.md, | ||
| ), | ||
| child: Row( | ||
| crossAxisAlignment: CrossAxisAlignment.stretch, | ||
| children: [ | ||
| const Expanded(flex: 1, child: _VersionCard()), | ||
| const SizedBox(width: AppSpacing.md), | ||
| Expanded( | ||
| flex: 1, | ||
| child: Column( | ||
| children: const [ | ||
| Expanded(flex: 2, child: _SupportCallout()), | ||
| SizedBox(height: AppSpacing.xs), | ||
| Expanded(flex: 1, child: _DiscordCallout()), | ||
| SizedBox(height: AppSpacing.xs), | ||
| Expanded(flex: 1, child: _AnnouncementCard()), | ||
| ], | ||
| return Padding( | ||
| padding: const EdgeInsets.fromLTRB( | ||
| AppSpacing.lg, | ||
| 0, | ||
| AppSpacing.lg, | ||
| AppSpacing.md, | ||
| ), | ||
| child: Column( | ||
| crossAxisAlignment: CrossAxisAlignment.stretch, | ||
| children: [ | ||
| Row( | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| children: [ | ||
| Expanded( | ||
| child: SizedBox( | ||
| height: _versionHeight, | ||
| child: const _VersionCard(), | ||
| ), | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| const SizedBox(width: AppSpacing.md), | ||
| Expanded( | ||
| child: Column( | ||
| children: const [ | ||
| SizedBox( | ||
| height: _smallCardHeight, | ||
| child: _DiscordCallout(), | ||
| ), | ||
| SizedBox(height: AppSpacing.xs), | ||
| SizedBox( | ||
| height: _smallCardHeight, | ||
| child: _AnnouncementCard(), | ||
| ), | ||
| SizedBox(height: AppSpacing.xs), | ||
| SizedBox(height: _smallCardHeight, child: _StatusCard()), | ||
| ], | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| const SizedBox(height: AppSpacing.md), | ||
| SizedBox(height: _smallCardHeight, child: const _SupportCallout()), | ||
| ], | ||
| ), | ||
| ); | ||
| } |
| static List<Color> _hashGradient(String seed, Brightness brightness) { | ||
| var h = 7; | ||
| for (final rune in seed.runes) { | ||
| h = (h * 31 + rune) & 0x7fffffff; | ||
| } | ||
| final base = h % 360; | ||
| const saturation = 0.62; | ||
| final light = brightness == Brightness.dark ? 0.70 : 0.46; | ||
| return [ | ||
| HSLColor.fromAHSL(1, base.toDouble(), saturation, light).toColor(), | ||
| HSLColor.fromAHSL( | ||
| 1, | ||
| (base + 137.508) % 360, | ||
| saturation, | ||
| light - 0.10, | ||
| ).toColor(), | ||
| ]; | ||
| } |
| final end = _time.format( | ||
| widget.frames[_liveIndex].time.toLocal().add(period), | ||
| ); |
There was a problem hiding this comment.
當 widget.timeFormat 與預設的 _time 不同時,start 會使用 widget.timeFormat 格式化,而 end 會使用 _time 格式化,導致顯示的範圍標籤格式不一致。建議在計算 end 時也使用 widget.timeFormat ?? _time。
Suggestion:
| final end = _time.format( | |
| widget.frames[_liveIndex].time.toLocal().add(period), | |
| ); | |
| final format = widget.timeFormat ?? _time; | |
| final end = format.format( | |
| widget.frames[_liveIndex].time.toLocal().add(period), | |
| ); |
New(zh-Hant): 版本卡片正式版也會顯示完整版本號 New(en-US): The hero version card now shows a version line for releases too
New(zh-Hant): 新版號 26.2.1 的版型只顯示 26.2 New(en-US): A three-part release (26.2.1) advertises as 26.2; the patch stays in the label
New(zh-Hant): 版本卡片徽章加上 build 日期 New(en-US): The More page badge shows the day the build was cut
New(zh-Hant): 版本徽章右側顯示 build 日期 New(en-US): The version badge now carries the day this build was cut
| static Future<void> ensureLoaded() async { | ||
| if (_label != null) return; | ||
| String platformVersion = ''; |
There was a problem hiding this comment.
ensureLoaded 方法目前的冪等性檢查依賴於 _label != null。如果 _bestLabel 為空或 _bestCode 不大於 0(例如在某些開發環境下),_label 將保持為 null,導致每次呼叫 ensureLoaded 都會重新執行非同步的 PackageInfo.fromPlatform()。建議使用一個獨立的布林值(如 _isLoaded)來標記載入狀態,以避免重複的 IO 操作。
Suggestion:
| static Future<void> ensureLoaded() async { | |
| if (_label != null) return; | |
| String platformVersion = ''; | |
| static bool _isLoaded = false; | |
| static Future<void> ensureLoaded() async { | |
| if (_isLoaded) return; | |
| String platformVersion = ''; | |
| try { | |
| final info = await PackageInfo.fromPlatform(); | |
| platformVersion = info.version; | |
| } on Object { | |
| // A version readout is never worth failing a launch over. The platform | |
| // version line simply stays empty for that build. | |
| } | |
| _platformVersion = platformVersion; | |
| if (_bestLabel.isNotEmpty && _bestCode > 0) { | |
| _label = _bestLabel; | |
| _code = _bestCode; | |
| } | |
| _isLoaded = true; | |
| } |
| child: Column( | ||
| children: const [ | ||
| SizedBox( | ||
| height: _smallCardHeight, | ||
| child: _DiscordCallout(), | ||
| ), | ||
| SizedBox(height: AppSpacing.xs), | ||
| SizedBox( | ||
| height: _smallCardHeight, | ||
| child: _AnnouncementCard(), | ||
| ), | ||
| SizedBox(height: AppSpacing.xs), | ||
| SizedBox(height: _smallCardHeight, child: _StatusCard()), | ||
| ], | ||
| ), |
There was a problem hiding this comment.
_HeroCards 的佈局使用了固定的高度(如 _versionHeight 和 _smallCardHeight)來包裹包含文字內容的組件(例如 _DiscordCallout、_AnnouncementCard、_StatusCard 和 _VersionCard)。這在 Flutter 中存在嚴重的佈局溢出(Overflow)風險,特別是當使用者在系統設定中啟用了「大字體模式」(Font Scaling),或者在不同語言的本地化過程中,文字內容長度增加導致高度超過預設值時。建議考慮使用 IntrinsicHeight 來讓 Row 中的元素高度同步,或者改用 BoxConstraints 的 minHeight 而非固定 height,以允許組件根據內容自動增長。
| static List<Color> _hashGradient(String seed, Brightness brightness) { | ||
| var h = 7; | ||
| for (final rune in seed.runes) { | ||
| h = (h * 31 + rune) & 0x7fffffff; | ||
| } | ||
| final base = h % 360; | ||
| const saturation = 0.62; | ||
| final light = brightness == Brightness.dark ? 0.70 : 0.46; | ||
| return [ | ||
| HSLColor.fromAHSL(1, base.toDouble(), saturation, light).toColor(), | ||
| HSLColor.fromAHSL( | ||
| 1, | ||
| (base + 137.508) % 360, | ||
| saturation, | ||
| light - 0.10, | ||
| ).toColor(), | ||
| ]; | ||
| } |
| @override | ||
| String get moreTagline => 'Platform Integrasi Informasi Bencana'; |
There was a problem hiding this comment.
印尼文翻譯與英文原文語意不一致。英文定義為 'Disaster Prevention Information Platform' (災害預防資訊平台),但目前的印尼文翻譯為 'Platform Integrasi Informasi Bencana' (災害資訊整合平台),其中 'Integrasi' 意為 '整合' 而非 '預防' (Pencegahan)。建議修正以符合原文語意。
Suggestion:
| @override | |
| String get moreTagline => 'Platform Integrasi Informasi Bencana'; | |
| @override | |
| String get moreTagline => 'Platform Informasi Pencegahan Bencana'; |
| final utc = DateTime.utc(2026, 7, 13, 14, 30); // 22:30 in UTC+8 | ||
| final frames = [MapFrame(id: '0', time: utc)]; | ||
| await tester.pumpWidget( | ||
| _wrap(frames: frames, selectedIndex: 0, onSelected: (_) {}), | ||
| ); |
There was a problem hiding this comment.
該測試案例高度依賴於執行環境的時區。若在 CI 環境(通常時區設定為 UTC)中執行,utc.toLocal() 的結果會與 utc 完全相同。這意味著即使程式碼錯誤地直接顯示了 UTC 時間,測試依然會通過,從而無法達到攔截 Bug 的目的。建議選擇一個在轉換為本地時間後會明顯改變小時或日期的時間點(例如接近 UTC 當天結尾的時間),以增加測試在不同環境下的魯棒性。
Suggestion:
| final utc = DateTime.utc(2026, 7, 13, 14, 30); // 22:30 in UTC+8 | |
| final frames = [MapFrame(id: '0', time: utc)]; | |
| await tester.pumpWidget( | |
| _wrap(frames: frames, selectedIndex: 0, onSelected: (_) {}), | |
| ); | |
| // 建議選擇一個轉換後容易產生差異的時間點,例如 UTC 23:30 | |
| final utc = DateTime.utc(2026, 7, 13, 23, 30); | |
| final frames = [MapFrame(id: '0', time: utc)]; | |
| await tester.pumpWidget( | |
| _wrap(frames: frames, selectedIndex: 0, onSelected: (_) {}), | |
| ); |
New(zh-Hant): 首頁地區列下方浮一條金色支持橫條,點擊前往贊助頁 New(en-US): Home floats a gold support pill under the region bar, opening the sponsor page
New(zh-Hant): 繁體贊助相關文案改用「支援」 New(en-US): Traditional-Chinese support copy now reads 支援, not 支持
New(zh-Hant): README 新增 Android 測試版與 iOS TestFlight 連結 New(en-US): README now links the Android testing track and the iOS TestFlight beta
Fix(zh-Hant): 修正公測使用者可能被當成正式版使用者,收到比手上還舊的更新提示 Fix(en-US): fix a beta tester being treated as a stable user and offered an older build `AppVersion.tryParse` stops at the first letter, so every snapshot of a year parses to the same number — `26w34a` and `26w40a` are both `26`. The channel was decided by matching that against a release tag, which finds *a* release of that year rather than the one running, and only gave the right answer because every `26w**` release happens to be a pre-release. It breaks as soon as `v26.1` exists, or as soon as the running snapshot falls off the fetched page. The fallback then decides, and it cannot help on Android: internal, open testing and production all install through `com.android.vending`. The channel is now read from the build ordinal, which is what identifies one build; the version string stays as the path for builds older than the scheme.
| @override | ||
| String get moreTagline => 'Platform Integrasi Informasi Bencana'; |
There was a problem hiding this comment.
印尼文翻譯建議確認語意一致性。目前的翻譯為 "災害資訊整合平台" (Platform Integrasi Informasi Bencana),而英文版本為 "Disaster Prevention Information Platform",中文版本為 "防災資訊整合平台",兩者皆強調了「預防/防災」(Prevention) 的含義。建議確認是否應加入預防含義(例如使用 "Pencegahan Bencana"),以保持各語言間語意的一致性。
Suggestion:
| @override | |
| String get moreTagline => 'Platform Integrasi Informasi Bencana'; | |
| @override | |
| String get moreTagline => 'Platform Integrasi Informasi Pencegahan Bencana'; |
| @override | ||
| String get moreTagline => '防灾信息整合平台'; |
New(zh-Hant): 更新日誌每張版本卡片下方新增 Contributors 頭像列 New(en-US): changelog cards foot a Contributors avatar strip The strip is parsed from @Handles already in the release body, so no extra API call; avatars stream through the ETag store as URL-addressed assets like map tiles, so revisits are local reads.
A merge commit is invisible to the commit gate — `check_commits.sh` walks with `--no-merges`, because a merge message is generated rather than written — so anything that arrives through one is never judged. A branch that is behind is a different hole: the gates that passed describe a tree nobody will ever have. The message gate also now walks the branch's own tip rather than the checked-out merge commit, and takes the base ref through the environment: a pull request from a fork carries whatever branch name its author chose, and that was being pasted into the shell.
| return out; | ||
| } | ||
|
|
||
| final RegExp _atHandle = RegExp(r'@([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)'); |
| final shown = contributors.take(_maxShown).toList(); | ||
| final avatarWidth = 26 * shown.length - 6 * (shown.length - 1); | ||
| return Padding( | ||
| padding: const EdgeInsets.fromLTRB( | ||
| AppSpacing.lg, | ||
| AppSpacing.sm, | ||
| AppSpacing.lg, | ||
| AppSpacing.md, | ||
| ), | ||
| child: Row( | ||
| children: [ | ||
| SizedBox( | ||
| width: avatarWidth.toDouble(), | ||
| height: 26, | ||
| child: Stack( | ||
| clipBehavior: Clip.none, | ||
| children: [ | ||
| for (var i = 0; i < shown.length; i++) | ||
| Positioned( | ||
| left: (i * 20).toDouble(), | ||
| child: _Avatar(contributor: shown[i]), | ||
| ), | ||
| ], | ||
| ), | ||
| ), |
| builder: (context, dials, _) => SafeArea( | ||
| bottom: false, | ||
| child: Column( | ||
| children: [ | ||
| RegionBar( | ||
| blend: dials.blend, | ||
| dismiss: dials.dismiss, | ||
| skyIsLight: skyIsLightFrom(sky, weatherMode), | ||
| ), | ||
| _GoldSupportBar( | ||
| blend: dials.blend, | ||
| dismiss: dials.dismiss, | ||
| ), | ||
| ], | ||
| ), | ||
| ), |
There was a problem hiding this comment.
使用 SafeArea 包裹 Column 可能會導致 RegionBar 的佈局位置發生非預期的偏移。由於 RegionBar 位於 Column 的頂部,而 SafeArea 預設會根據設備的狀態列(Status Bar)添加頂部內邊距,這可能導致原本緊貼螢幕頂部的 RegionBar 被向下推移,破壞 UI 設計。建議確認是否需要 top: false。
Suggestion:
| builder: (context, dials, _) => SafeArea( | |
| bottom: false, | |
| child: Column( | |
| children: [ | |
| RegionBar( | |
| blend: dials.blend, | |
| dismiss: dials.dismiss, | |
| skyIsLight: skyIsLightFrom(sky, weatherMode), | |
| ), | |
| _GoldSupportBar( | |
| blend: dials.blend, | |
| dismiss: dials.dismiss, | |
| ), | |
| ], | |
| ), | |
| ), | |
| builder: (context, dials, _) => SafeArea( | |
| bottom: false, | |
| top: false, // 建議明確設定 top: false 以確保 RegionBar 仍可緊貼頂部 | |
| child: Column( | |
| children: [ | |
| RegionBar( | |
| blend: dials.blend, | |
| dismiss: dials.dismiss, | |
| skyIsLight: skyIsLightFrom(sky, weatherMode), | |
| ), | |
| _GoldSupportBar( | |
| blend: dials.blend, | |
| dismiss: dials.dismiss, | |
| ), | |
| ], | |
| ), | |
| ), |
| class _GoldSupportBar extends StatelessWidget { | ||
| const _GoldSupportBar({required this.blend, required this.dismiss}); | ||
|
|
||
| final double blend; | ||
| final double dismiss; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| final l10n = AppLocalizations.of(context); | ||
| final gold = AppGold.of(context); | ||
| final hidden = (blend + dismiss).clamp(0.0, 1.0); | ||
| return IgnorePointer( | ||
| ignoring: hidden > 0.9, | ||
| child: Opacity( | ||
| opacity: 1 - hidden, | ||
| child: FractionalTranslation( | ||
| translation: Offset(0, -6 * dismiss), | ||
| child: Material( | ||
| color: gold.badge, | ||
| borderRadius: BorderRadius.zero, | ||
| clipBehavior: Clip.antiAlias, | ||
| child: InkWell( | ||
| onTap: () => context.pushNamed(AppRoutes.sponsor), | ||
| child: SizedBox( | ||
| height: 30, | ||
| child: Row( | ||
| mainAxisAlignment: MainAxisAlignment.center, | ||
| mainAxisSize: MainAxisSize.max, | ||
| children: [ | ||
| Flexible( | ||
| child: Text( | ||
| l10n.sponsorTitle, | ||
| maxLines: 1, | ||
| overflow: TextOverflow.ellipsis, | ||
| textAlign: TextAlign.center, | ||
| style: Theme.of(context).textTheme.labelMedium | ||
| ?.copyWith( | ||
| color: gold.onBadge, | ||
| fontWeight: FontWeight.w800, | ||
| ), | ||
| ), | ||
| ), | ||
| const SizedBox(width: 2), | ||
| Icon( | ||
| Icons.chevron_right, | ||
| size: 16, | ||
| color: gold.onBadge.withValues(alpha: 0.85), | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
| } |
| final hidden = (blend + dismiss).clamp(0.0, 1.0); | ||
| return IgnorePointer( | ||
| ignoring: hidden > 0.9, | ||
| child: Opacity( | ||
| opacity: 1 - hidden, | ||
| child: FractionalTranslation( |
| @override | ||
| String get moreTagline => 'Disaster Prevention Information Platform'; |
| @override | ||
| String get moreTagline => | ||
| 'Platform para sa Integral na Impormasyon sa Kalamidad'; |
| @override | ||
| String get moreTagline => '防災資訊整合平台'; |
| final label = AppBuild.label; | ||
| final stable = RegExp(r'^\d+\.\d+$').hasMatch(label); | ||
| expect(find.text(AppBuild.train), findsWidgets); | ||
| if (stable) { |
| expect(label, isNot(v['train'])); | ||
| } else { | ||
| expect(label, v['train']); | ||
| expect(v['train'], matches(r'^\d+\.\d+$')); |
There was a problem hiding this comment.
測試斷言被弱化了。原本的測試會驗證當 label 是兩部分版本(例如 26.1)時,它必須與 train 相等。修改後的版本僅檢查 train 的格式,失去了對 label 與 train 一致性的驗證。根據程式碼註解「A two-part label (26.1) already is its own train」,兩者應該是相等的。
Suggestion:
| expect(v['train'], matches(r'^\d+\.\d+$')); | |
| expect(v['train'], label); | |
| expect(label, matches(r'^\d+\.\d+$')); |
Fix(zh-Hant): 修正在「更多」點開 App 日誌後畫面狂刷並卡死 Fix(en-US): fix the app freezing after opening the log screen from More Reporting an error is not free of consequence: it goes to Talker, whose stream the log screen rebuilds on and the persister writes to disk from. So a fault raised *while rendering that screen* — a layout overflow is the everyday one — re-enters through the rebuild it just caused, and every turn adds a Crashlytics report marked fatal and a database write. The screen already dodged one known overflow by not using Talker's own app bar, but dodging one does not close the circle. A repeated fault is now reported eight times and then dropped until it stops for five seconds, per fault, so a distinct error is never hidden by another. The notice saying so goes through `info`, which cannot be the next turn of the loop. The screen also no longer rebuilds once per line: it takes the history on a 400 ms tick, so a list cannot rebuild under a finger that is scrolling it.
Optimization(zh-Hant): App 日誌改用內建畫面,多了搜尋、等級篩選與分享 Optimization(en-US): the log screen gains search, level filtering and sharing The hand-rolled layout existed to dodge an overflow in `TalkerScreen`'s header, because an overflow is logged and the screen rebuilt on every line — a loop that ended in a hang. That loop is cut where it starts now, so rebuilding the screen ourselves bought nothing and cost the search, the filter, the sharing and the settings that come with the real one. Three l10n keys went with it. The row ceiling drops from 20000 to 5000 and moves into the insert's own transaction. It was only applied by the hourly sweep, and a fault that logs every frame writes faster than any sweep runs — so the table could not be bounded between them, which is exactly the case the ceiling is for.
Fix(zh-Hant): 修正日誌頁把同一個錯誤不斷印到主控台,回放的紀錄也不再遺失等級 Fix(en-US): stop the log screen reprinting one fault forever, and keep replayed levels Two faults, both mine, both from the change that stopped the screen freezing. The repeat check sat *after* `FlutterError.presentError`, so suppression covered the log, the crash report and the database but not the terminal: the same fault printed every frame while the stored record stayed clean, which is exactly how it was described — the data was fine and the console was unusable. It is checked first now, and the first few still print, because that dump is what makes a layout fault findable. And a replayed line was titled `stored`, which threw away the level it was written with. Talker colours a card and the level filter narrows by `logLevel`, so every line read back out of the table was uncoloured and unfilterable — the two things the screen is read with.
7875cde to
bebb1e4
Compare
Fix(zh-Hant): 修正開啟日誌頁會把整份紀錄重印到主控台,並重複寫回資料庫 Fix(en-US): opening the log no longer reprints the whole table or writes it back `logCustom` was the obvious way to get stored lines onto the screen and the wrong one. It publishes to Talker's stream — which the persister writes from — and prints to the console when console logs are on. So opening the screen reprinted the entire table to the terminal *and* wrote every line back into the table it had just been read from, leaving a duplicate behind on each visit. The read limit made it worse by asking for 5000 lines into a history that holds 1000, so most of that work was evicted as it arrived. `Log.replay` writes to Talker's history and nothing else. The screen is built only once the replay is in, because history is read at build time. A replayed card also read `log`, because that is what `TalkerData.title` defaults to; it carries the level's own name now.
Fix(zh-Hant): 修正日誌頁的清除只清畫面,重開又整份回來 Fix(en-US): clearing the log no longer leaves everything to come back on reopen The screen's clear button calls `talker.cleanHistory()`, which empties the in-memory list and nothing else. The screen replays the `logs` table on every visit, so everything came straight back from a table the button never touched — the one thing it is pressed for is the one thing it did not do. Talker's history is an interface, and this app already supplies its own, so clearing it now clears the table with it.
Fix(zh-Hant): 修正日誌頁上方等級篩選器把回放的紀錄全算成 undefined Fix(en-US): the level filter counts replayed lines under their level again The screen groups its filter chips — and their counts — by `TalkerData.key`, and colours a card by the same value. Not by the level, and not by the title. A replayed line carried neither, so every stored line of every level collapsed into one chip labelled `undefined`, uncoloured. `Log.replay` also fills in the title and pen from that key, which is what the logger does on the way past and what skipping it skipped.
Fix(zh-Hant): 修正開啟日誌頁會把本次啟動的紀錄擠掉,順序也錯亂 Fix(en-US): opening the log no longer pushes out the running session's lines Talker's history appends and evicts from the front, so replaying a day of stored lines pushed the live session out — `DPIP starting up` among them — and left the older lines sitting where the newer ones should be. Reading 1000 into a history that holds exactly 1000 made it certain. That is backwards twice over. The stored log is on disk and can be read again, the running session cannot; and lines older than everything in memory belong in front of it, not appended after. Replay now inserts at the front and only into free space.
Fix(zh-Hant): 修正啟動早期的紀錄從來沒被存下來,當機時最該看的那幾行都不見 Fix(en-US): fix the launch's own log lines never being stored, the ones a crash needs Merging the stored log into what was already in memory produced a fault every time it was touched: a loaded line evicting the running session, older lines sitting after newer ones, a duplicate left behind on each visit. All of it came from keeping two records and reconciling them. There is only one record. Every line is persisted, so memory held nothing the table did not — except the lines from before the database opened, which is the gap that made replacing unsafe and is a hole in its own right: `DPIP starting up` is logged at bootstrap.dart:111 and the store opens at :139, so everything that explains a crash during launch was in memory and nowhere else. Those are copied in now, and the screen loads the table over the top instead of merging.
Optimization(zh-Hant): 日誌的等級標籤改成大寫,一眼看得出哪一行不是 INFO Optimization(en-US): log lines are tagged INFO / WARN / ERROR, readable at a glance A level is a label, not prose, and it reads as a column when a hundred lines are scanned for the one that is not `INFO`. `WARN` rather than `WARNING` keeps the five that matter within a character of each other, so the messages after them line up. Display only. The `level` column still stores the enum's own name, so a stored line parses back to the level it was written with.
Optimization(zh-Hant): 主控台的日誌改成一則一行,不再有框線與跑掉的顏色碼 Optimization(en-US): console logs are one plain line each, without borders or stray colour codes The default logger draws every entry inside a box and paints it with ANSI escapes. Neither survives the trip: `flutter run` prefixes each line with `flutter: `, so a three-line box becomes three prefixed lines around one message, and the escapes arrive as the literal text `^[[38;5;4m` because nothing on that pipe reads them. What was meant as colour read as noise. Colour was never delivered rather than lost — `enableColors` can go back on if the output is ever read somewhere that renders it.
Optimization(zh-Hant): 主控台的等級標籤可以上色,用 --dart-define=DPIP_LOG_COLOR=true 開啟 Optimization(en-US): the console level tag can be coloured with --dart-define=DPIP_LOG_COLOR=true A choice rather than a detection, because it cannot be detected: the bytes are written on the device and read in whatever window is attached to `flutter run`, which the app cannot see. VS Code's Debug Console prints ANSI literally — that is where `^[[38;5;4m` came from — and its integrated terminal renders it. Same build, different window. `TalkerLogger` forces `ansiColorDisabled = false` in its constructor whatever stdout is, which is why the escapes were emitted to a pipe that could not read them at all. Off by default now, and only the tag is painted when it is on: a fully coloured line is harder to read than a plain one, and a leak then costs one short token instead of the whole line.
Fix(zh-Hant): 修正 iOS 上開啟主控台顏色只會多出跳脫字元,看不到顏色 Fix(en-US): enabling console colour on iOS added escape characters instead of colour The flag was written as a choice because a terminal's capability cannot be detected from inside the app. On iOS there is nothing to choose: the platform's log path escapes the escape character itself, so a terminal that does support ANSI still receives a backslash followed by the sequence and prints it. That is flutter/flutter#20663, and it is why turning the flag on looked like it did nothing but add noise. That part *is* detectable, so it is detected. Elsewhere the flag still means what it said.
Optimization(zh-Hant): 新增 tool/colorize_logs.sh,在終端機端替日誌上色 Optimization(en-US): add tool/colorize_logs.sh, which colours the log in the terminal The app writes plain text and should keep doing so. On iOS an escape sequence cannot survive the trip: the platform's log path escapes the escape character itself, so a terminal that fully supports ANSI still receives a backslash followed by the sequence and prints it — flutter/flutter#20663. `dart:developer`'s `log` does deliver them, but truncates anything past ~128 characters to `<collected>`, and the long lines are the diagnostic ones, so it buys colour with the content. A pipe has neither problem, because the bytes are written by the terminal that knows whether ANSI works — and it turns itself off when the output is not one. It drops the `flutter: ` prefix too, which is a terminal's width back. mise exec -- flutter run | tool/colorize_logs.sh
Optimization(zh-Hant): 新增 tool/run.sh,一個指令跑起來就有上色的日誌
Optimization(en-US): add tool/run.sh so one command runs the app with a coloured log
tool/run.sh -d "iPhone 17 Pro"
Hot reload still works. The tool reads `supportsColor` from stdout and its
keystrokes from stdin, and a pipe only touches the first — what is lost is
flutter's own colour and its progress spinner, which are redraw sequences a
pipe turns into litter anyway.
`pipefail` is the part a wrapper like this usually gets wrong: without it the
pipeline reports the colouriser's status, a failed build exits 0, and the
wrapper hides the thing it wraps. A test builds a stub that exits 7 and checks
the wrapper does too.
Fix(zh-Hant): 修正結束執行時出現 Broken pipe 的未處理例外 Fix(en-US): fix the unhandled Broken pipe exception when a run is stopped Ctrl-C reaches every process in the foreground group, so the filter died first and `flutter run` — still shutting down, still printing — wrote into a closed pipe and reported EPIPE as an unhandled exception, stack trace and all. The filter now ignores the interrupt and reads until its stdin closes, which is when the writer has genuinely finished. `tool/run.sh` is also the only supported way to start the app now, and a debug build says so when it was started any other way. Both alternatives work and both are wrong invisibly: a bare `flutter run` resolves whatever SDK the shell's PATH cached — `mise activate` does not refresh it when mise.toml changes — and the log arrives uncoloured either way.
這個 PR 做了什麼
相關 issue
怎麼驗
檢查清單
tool/check_commits.sh origin/main..HEAD通過—— commit 訊息就是更新日誌,格式見 commit.md
mise exec -- flutter analyze與mise exec -- flutter test通過AppLocalizations,沒有寫死AppSpacing/AppRadius/AppMotion,深色模式看過,文字對比度可接受